/*
 * @File     : ffm3u8_open.js.js
 * @Author   : jade
 * @Date     : 2024/2/5 16:06
 * @Email    : jadehh@1ive.com
 * @Software : Samples
 * @Desc     :
 */
import { _ } from '../encodings/cat';
import * as HLS from './hls';

let key = 'ffm3u8';
let url = '';
let categories = [];
let siteKey = '';
let siteType = 0;

async function request(reqUrl, agentSp) {
  let res = await req(reqUrl, {
    method: 'get',
  });
  return JSON.parse(res.content);
}

async function init(cfg) {
  siteKey = cfg.skey;
  siteType = cfg.stype;
  url = cfg.ext.url;
  categories = cfg.ext.categories;
}

async function home(filter) {
  const data = await request(url);
  let classes = [];
  for (const cls of data.class) {
    const n = cls.type_name.toString().trim();
    if (categories && categories.length > 0) {
      if (categories.indexOf(n) < 0) continue;
    }
    classes.push({
      type_id: cls.type_id.toString(),
      type_name: n,
    });
  }
  if (categories && categories.length > 0) {
    classes = _.sortBy(classes, (p) => {
      return categories.indexOf(p.type_name);
    });
  }
  return {
    class: classes,
  };
}

async function homeVod() {
  return '{}';
}

async function category(tid, pg, filter, extend) {
  let page = pg || 1;
  if (page == 0) page = 1;
  const data = await request(url + `?ac=detail&t=${tid}&pg=${page}`);
  let videos = [];
  for (const vod of data.list) {
    videos.push({
      vod_id: vod.vod_id.toString(),
      vod_name: vod.vod_name.toString(),
      vod_pic: vod.vod_pic,
      vod_remarks: vod.vod_remarks,
    });
  }
  return {
    page: parseInt(data.page),
    pagecount: data.pagecount,
    total: data.total,
    list: videos,
  };
}

async function detail(id) {
  const data = (await request(url + `?ac=detail&ids=${id}`)).list[0];
  let vod = {
    vod_id: data.vod_id,
    vod_name: data.vod_name,
    vod_pic: data.vod_pic,
    type_name: data.type_name,
    vod_year: data.vod_year,
    vod_area: data.vod_area,
    vod_remarks: data.vod_remarks,
    vod_actor: data.vod_actor,
    vod_director: data.vod_director,
    vod_content: data.vod_content.trim(),
    vod_play_from: data.vod_play_from,
    vod_play_url: data.vod_play_url,
  };
  return {
    list: [vod],
  };
}

async function proxy(segments, headers, reqHeaders) {
  let what = segments[0];
  let segs = decodeURIComponent(segments[1]);
  if (what == 'hls') {
    function hlsHeader(data, hls) {
      let hlsHeaders = {};
      if (data.headers['content-length']) {
        Object.assign(hlsHeaders, data.headers, { 'content-length': hls.length.toString() });
      } else {
        Object.assign(hlsHeaders, data.headers);
      }
      delete hlsHeaders['transfer-encoding'];
      if (hlsHeaders['content-encoding'] == 'gzip') {
        delete hlsHeaders['content-encoding'];
      }
      return hlsHeaders;
    }
    const hlsData = await hlsCache(segs, headers);
    if (hlsData.variants) {
      // variants -> variants -> .... ignore
      const hls = HLS.stringify(hlsData.plist);
      return {
        code: hlsData.code,
        content: hls,
        headers: hlsHeader(hlsData, hls),
      };
    } else {
      const hls = HLS.stringify(hlsData.plist, (segment) => {
        return js2Proxy(
          false,
          siteType,
          siteKey,
          'ts/' + encodeURIComponent(hlsData.key + '/' + segment.mediaSequenceNumber.toString()),
          headers,
        );
      });
      return {
        code: hlsData.code,
        content: hls,
        headers: hlsHeader(hlsData, hls),
      };
    }
  } else if (what == 'ts') {
    const info = segs.split('/');
    const hlsKey = info[0];
    const segIdx = parseInt(info[1]);
    return await tsCache(hlsKey, segIdx, headers);
  }
  return '{}';
}

async function play(flag, id, flags) {
  try {
    const pUrls = await hls2Urls(id, {});
    for (let index = 1; index < pUrls.length; index += 2) {
      pUrls[index] = js2Proxy(false, siteType, siteKey, 'hls/' + encodeURIComponent(pUrls[index]), {});
    }
    pUrls.push('original');
    pUrls.push(id);
    return {
      parse: 0,
      url: pUrls,
    };
  } catch (e) {
    return {
      parse: 0,
      url: id,
    };
  }
}

async function search(wd, quick, pg) {
  let page = pg || 1;
  if (page == 0) page = 1;
  const data = await request(url + `?ac=detail&wd=${wd}`);
  let videos = [];
  for (const vod of data.list) {
    videos.push({
      vod_id: vod.vod_id.toString(),
      vod_name: vod.vod_name.toString(),
      vod_pic: vod.vod_pic,
      vod_remarks: vod.vod_remarks,
    });
  }
  return {
    page: parseInt(data.page),
    pagecount: data.pagecount,
    total: data.total,
    list: videos,
  };
}

const cacheRoot = 'hls_cache';
const hlsKeys = [];
const hlsPlistCaches = {};
const interrupts = {};
const downloadTask = {};
let currentDownloadHlsKey = '';

function hlsCacheInsert(key, data) {
  hlsKeys.push(key);
  hlsPlistCaches[key] = data;
  if (hlsKeys.length > 5) {
    const rmKey = hlsKeys.shift();
    hlsCacheRemove(rmKey);
  }
}

function hlsCacheRemove(key) {
  delete hlsPlistCaches[key];
  delete hlsKeys[key];
  new JSFile(cacheRoot + '/' + key).delete();
}

function plistUriResolve(baseUrl, plist) {
  if (plist.variants) {
    for (const v of plist.variants) {
      if (!v.uri.startsWith('http')) {
        v.uri = relative2Absolute(baseUrl, v.uri);
      }
    }
  }
  if (plist.segments) {
    for (const s of plist.segments) {
      if (!s.uri.startsWith('http')) {
        s.uri = relative2Absolute(baseUrl, s.uri);
      }
      if (s.key && s.key.uri && !s.key.uri.startsWith('http')) {
        s.key.uri = relative2Absolute(baseUrl, s.key.uri);
      }
    }
  }
  return plist;
}

async function hls2Urls(url, headers) {
  let urls = [];
  let resp = {};
  let tmpUrl = url;
  while (true) {
    resp = await req(tmpUrl, {
      headers: headers,
      redirect: 0,
    });
    if (resp.headers['location']) {
      tmpUrl = resp.headers['location'];
    } else {
      break;
    }
  }
  if (resp.code == 200) {
    var hls = resp.content;
    const plist = plistUriResolve(tmpUrl, HLS.parse(hls));
    if (plist.variants) {
      for (const vari of _.sortBy(plist.variants, (v) => -1 * v.bandwidth)) {
        urls.push(`proxy_${vari.resolution.width}x${vari.resolution.height}`);
        urls.push(vari.uri);
      }
    } else {
      urls.push('proxy');
      urls.push(url);
      const hlsKey = md5X(url);
      hlsCacheInsert(hlsKey, {
        code: resp.code,
        plist: plist,
        key: hlsKey,
        headers: resp.headers,
      });
    }
  }
  return urls;
}

async function hlsCache(url, headers) {
  const hlsKey = md5X(url);
  if (hlsPlistCaches[hlsKey]) {
    return hlsPlistCaches[hlsKey];
  }
  let resp = {};
  let tmpUrl = url;
  while (true) {
    resp = await req(tmpUrl, {
      headers: headers,
      redirect: 0,
    });
    if (resp.headers['location']) {
      tmpUrl = resp.headers['location'];
    } else {
      break;
    }
  }
  if (resp.code == 200) {
    var hls = resp.content;
    const plist = plistUriResolve(tmpUrl, HLS.parse(hls));
    hlsCacheInsert(hlsKey, {
      code: resp.code,
      plist: plist,
      key: hlsKey,
      headers: resp.headers,
    });
    return hlsPlistCaches[hlsKey];
  }
  return {};
}

async function tsCache(hlsKey, segmentIndex, headers) {
  if (!hlsPlistCaches[hlsKey]) {
    return {};
  }
  const plist = hlsPlistCaches[hlsKey].plist;
  const segments = plist.segments;

  let startFirst = !downloadTask[hlsKey];
  if (startFirst) {
    downloadTask[hlsKey] = {};
    for (const seg of segments) {
      const tk = md5X(seg.uri + seg.mediaSequenceNumber.toString());
      downloadTask[hlsKey][tk] = {
        file: cacheRoot + '/' + hlsKey + '/' + tk,
        uri: seg.uri,
        key: tk,
        index: seg.mediaSequenceNumber,
        order: seg.mediaSequenceNumber,
        state: -1,
        read: false,
      };
    }
  }

  // sort task
  for (const tk in downloadTask[hlsKey]) {
    const task = downloadTask[hlsKey][tk];
    if (task.index >= segmentIndex) {
      task.order = task.index - segmentIndex;
    } else {
      task.order = segments.length - segmentIndex + task.index;
    }
  }

  if (startFirst) {
    fixedCachePool(hlsKey, 5, headers);
  }

  const segment = segments[segmentIndex];
  const tsKey = md5X(segment.uri + segment.mediaSequenceNumber.toString());
  const task = downloadTask[hlsKey][tsKey];
  if (task.state == 1 || task.state == -1) {
    const file = new JSFile(task.file);
    if (await file.exist()) {
      task.state = 1;
      // download finish
      return {
        buffer: 3,
        code: 200,
        headers: {
          connection: 'close',
          'content-type': 'video/mp2t',
        },
        content: file,
      };
    } else {
      // file miss?? retry
      task.state = -1;
    }
  }
  if (task.state == -1) {
    // start download
    startTsTask(hlsKey, task, headers);
  }
  // wait read dwonload
  if (task.state == 0) {
    var stream = new JSProxyStream();
    stream.head(200, {
      connection: 'close',
      'content-type': 'video/mp2t',
    });
    let downloaded = 0;
    task.read = true;
    new Promise(async function (resolve, reject) {
      const f = new JSFile(task.file + '.dl');
      await f.open('r');
      (async function waitReadFile() {
        const s = await f.size();
        if (s > downloaded) {
          var downloadBuf = await f.read(s - downloaded, downloaded);
          await stream.write(downloadBuf);
          downloaded = s;
        }
        if (task.state == 1 || task.state < 0) {
          // finish error or done
          stream.done();
          await f.close();
          await f.delete();
          task.read = false;
          resolve();
          return;
        }
        setTimeout(waitReadFile, 5);
      })();
    });
    return {
      buffer: 3,
      content: stream,
    };
  }
}

async function startTsTask(hlsKey, task, headers) {
  if (task.state >= 0) return;
  if (!interrupts[hlsKey]) {
    return;
  }
  task.state = 0;
  if (await new JSFile(task.file).exist()) {
    task.state = 1;
    return;
  }
  const file = new JSFile(task.file + '.dl');
  await file.open('w');
  const resp = await req(task.uri, {
    buffer: 3,
    headers: headers,
    stream: file,
    timeout: [5000, 10000],
  });
  if (resp.error || resp.code >= 300) {
    await file.close();
    if (!task.read) {
      await file.delete();
    }
    task.state = -1;
    return;
  }
  await file.close();
  if (task.read) {
    await file.copy(task.file);
  } else {
    await file.move(task.file);
  }
  task.state = 1;
}

async function fixedCachePool(hlsKey, limit, headers) {
  // keep last cache task only
  if (currentDownloadHlsKey && currentDownloadHlsKey != hlsKey) {
    delete interrupts[currentDownloadHlsKey];
  }
  currentDownloadHlsKey = hlsKey;
  interrupts[hlsKey] = true;
  for (let index = 0; index < limit; index++) {
    if (!interrupts[hlsKey]) break;
    new Promise(function (resolve, reject) {
      (async function doTask() {
        if (!interrupts[hlsKey]) {
          resolve();
          return;
        }
        const tasks = _.pickBy(downloadTask[hlsKey], function (o) {
          return o.state == -1;
        });
        const task = _.minBy(Object.values(tasks), function (o) {
          return o.order;
        });
        if (!task) {
          resolve();
          return;
        }
        await startTsTask(hlsKey, task, headers);
        setTimeout(doTask, 5);
      })();
    });
  }
}

function relative2Absolute(base, relative) {
  var stack = base.split('/'),
    parts = relative.split('/');
  stack.pop();
  for (var i = 0; i < parts.length; i++) {
    if (parts[i] == '.') continue;
    if (parts[i] == '..') stack.pop();
    else stack.push(parts[i]);
  }
  return stack.join('/');
}
export { hls2Urls, hlsCache, tsCache };
